Elasticsearch篇之数据建模

Elasticsearch篇之数据建模

数据建模

  • 英文名为 Data Modeling, 为创建数据模型的过程
  • 数据模型 (Data Model)
    • 对现实世界进行抽象描述的一种工具和方法
    • 通过抽象的实体及实体之间的联系的形式去描述业务规则, 从而实现对现实世界的映射

数据建模的过程

  • 概念模型
    • 确定系统的核心需求和范围边界, 设计实体和实体间的关系
  • 逻辑模型
    • 进一步梳理业务需求, 确定每个实体的属性, 关系和约束等
  • 物理模型
    • 结合具体的数据库产品, 在满足业务读写性能等需求的前提下确定最终的定义
    • MySQL, MongoDB, Elasticsearch等
    • 第三范式

数据建模的意义

2020-04-15_225946

重视数据建模

  • 牵一发而动全身

ES中的数据建模

  • ES是基于Lucene以倒排索引为基础实现的储存体系, 不完全遵循关系型数据库中的范式约定

2020-04-15_230344

Mapping字段的相关设置

  • enabled
    • true
    • false
      • 仅存储, 不做搜索或聚合分析
      • 节省磁盘空间
      • 示例: 在web应用存储cookie, session等字段
  • index
    • true
    • false
      • 是否构建倒排索引
  • index_options
    • docs | freqs | positions | offsets
    • 存储倒排索引的哪些信息
  • norms
    • true
    • false
      • 是否存储归一化相关参数, 如果字段仅用于过滤和聚合分析(不进行相关性算分), 可关闭
  • doc_values
    • true
      • 用于排序和聚合分析
    • false
      • 仅用于搜索, 可关闭
  • field_data
    • false
    • true
      • 是否为text类型启用field_data, 实现排序和聚合分析
  • store
    • true
    • false
      • 不存储该字段值
  • coerce
    • true
      • 开启自动数据类型转换功能, 比如字符串转为数字, 浮点型转为整型
    • false
  • mutifields多字段
    • 灵活使用多字段特性来解决多样的业务需求
  • dynamic
    • true | false | strict
    • 控制mapping自动更新
  • date_detection
    • true | false
    • 是否自动识别日期类型

Mapping字段属性的设定流程

2020-04-15_232310

是何种类型?

  • 字符串类型
    • 需要分词则设定为text类型, 否则设置为keyword类型
  • 枚举类型
    • 基于性能考虑将其设置为keyword类型, 即便该数据为整型
  • 数值类型
    • 尽量选择贴近的类型, 比如byte即可表示所有数值时, 即选用byte, 不要用long
  • 其他类型
    • 比如布尔类型, 日期, 地理位置数据等

是否需要检索?

  • 完全不需要检索, 排序, 聚合分析的字段
    • enabled设置为false
  • 不需要检索的字段
    • index设置为false
  • 需要检索的字段, 可以通过如下配置设定需要的存储粒度
    • index_options结合需要设定
    • norms不需要归一化数据时关闭即可

是否需要排序和聚合分析?

  • 不需要排序或者聚合分析功能
    • doc_values 设定为false
    • fielddata 设定为false

是否需要另行存储

  • 是否需要专门存储当前字段的数据?
    • store 设定为true, 即可存储该字段的原始内容 (与_source中的不相关)
    • 一般结合_source的enabled设定为false时使用

实例

博客文章blog_index

  • 标题 title
  • 发布日期 publish_date
  • 作者 author
  • 摘要 abstract
  • 网络地址 url

Mapping设置

  • blog_index的mapping设置如下
2020-04-15_234332

索引主体变更

  • 当向blog_index添加一个 内容 content 时, 由于该字段内容有可能数据量非常大, 故将会带来以下问题
    • 在进行其他字段搜索时, 由于显示_source字段时把所有原始数据显示出来, 显示的content字段内容将会带来很大的性能问题

Mapping设置 (改进)

  • blog_index的mapping设置如下
2020-04-15_234836

​ 注意: 由于_source设置为了false, 故在设置url时不能简单的设置enabled为false

检索改进

2020-04-15_235434

演示

# request
# 设置blog_index的mapping (版本1)
PUT blog_index
{
  "mappings": {
    "doc":{
      "properties": {
        "title":{
          "type": "text",
          "fields": {
            "keyword":{
              "type":"keyword",
              "ignore_above": 100
            }
          }
        },
        "publish_date":{
          "type":"date"
        },
        "author":{
          "type":"keyword",
          "ignore_above": 100
        },
        "abstract":{
          "type": "text"
        },
        "url":{
          "enabled":false
        },
        "content":{
          "type":"text"
        }
      }
    }
  }
}

# 为blog_index索引添加文档
POST blog_index/doc/1
{
  "title": "blog title",
  "content": "blog content"
}

# 检索blog_index索引的数据
GET blog_index/_search
# response
# 可见由于_source字段把所有属性及其内容全部展示, 假设content数据内容非常大将及其耗费性能 (如果进行网络通信, 也将及其耗费带宽)
{
  "took": 56,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "blog_index",
        "_type": "doc",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "blog title",
          "content": "blog content"
        }
      }
    ]
  }
}

# 或许可以利用如下搜索
# 但此搜索的底层原理只是在已经检索出数据后进行简单的过滤, 在检索context时也避免不了性能问题
GET blog_index/_search?_source=title
# response
{
  "took": 64,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 1,
    "hits": [
      {
        "_index": "blog_index",
        "_type": "doc",
        "_id": "1",
        "_score": 1,
        "_source": {
          "title": "blog title"
        }
      }
    ]
  }
}

# 结合以上产生的问题, 解决思路如下所示
# 设置blog_index的mapping (版本2)
PUT blog_index
{
  "mappings": {
    "doc": {
      "_source": {
        "enabled": false
      },
      "properties": {
        "title": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 100
            }
          },
          "store": true
        },
        "publish_date": {
          "type": "date",
          "store": true
        },
        "author": {
          "type": "keyword",
          "ignore_above": 100, 
          "store": true
        },
        "abstract": {
          "type": "text",
          "store": true
        },
        "content": {
          "type": "text",
          "store": true
        },
        "url": {
          "type": "keyword",
          "doc_values":false,
          "norms":false,
          "ignore_above": 100, 
          "store": true
        }
      }
    }
  }
}

# 为blog_index索引添加文档
POST blog_index/doc/1
{
  "title": "blog title",
  "content": "blog content"
}

# 检索blog_index索引的数据
# 此时可以利用stored_fields有条件的进行检索
GET blog_index/_search
{
  "stored_fields": ["title"],
  "query":{
    "match": {
      "content": "blog"
    }
  },
  "highlight": {
    "fields": {"content": {}}
  }
}
# response
{
  "took": 567,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 1,
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "blog_index",
        "_type": "doc",
        "_id": "1",
        "_score": 0.2876821,
        "fields": {
          "title": [
            "blog title"
          ]
        },
        "highlight": {
          "content": [
            "blog content"
          ]
        }
      }
    ]
  }
}

关联关系处理

  • ES不擅长处理关系型数据库中的关联关系, 比如文章表blog与评论表comment之间通过blog_id关联, 在ES中可以通过如下两种手段变相解决
    • Nested Object
    • Parent/Child
  • 评论表comment
    • 文章Id blog_id
    • 评论人 username
    • 评论日期 date
    • 评论内容 content

文章表与评论表在关系型数据库中的表示

2020-04-16_002631

关联关系处理之Nested Object

  • ES中存储关联关系其中一个思路如下
2020-04-16_002912
  • 但是其实这样存储会带来一些问题, 如下:
2020-04-16_003011
  • 其原因是因为, Comment表默认存储为Object Array类型, 存储结构类似下面的形式:
2020-04-16_003148
  • Nested Object可以解决这个问题
2020-04-16_003442
  • 再次查询是将会得到正确的结果
2020-04-16_003538
  • Nested Object Array的存储结构类似下面的形式
2020-04-16_003646

关联关系处理之Parent/Child

  • ES还提供了类似于关系型数据库中join的实现方式, 使用join数据类型实现

2020-04-16_005405

  • 使用join创建文档
2020-04-16_005458
  • 常见query语法包括如下几种:

    • parent_id返回某父文档的子文档

    2020-04-16_005759

    • has_child返回包含某子文档的父文档

    2020-04-16_005832

    • has_parent返回包含某父文档的子文档

    2020-04-16_005908

Nested Object vs Parent/Child

2020-04-16_010133

Reindex

  • 指重建所有数据的过程, 一般发生在如下情况:
    • mapping 设置变更, 比如字段类型变化, 分词器字典更新等
    • index设置变更, 比如分片数更改等
    • 迁移数据
  • ES提供了现成的API用于完成该工作
    • _update_by_query在现有索引上重建
    • _reindex在其他索引上重建

_update_by_query

2020-04-16_011714

_reindex

2020-04-16_011754 2020-04-16_011831

Task

  • 数据重建的时间受索引文档规模的影响, 当规模越大时, 所需时间越多, 此时需要通过设定url参数 wait_for_completion为false来异步执行, ES以task来描述此类执行任务

  • ES提供了Task API来查看任务的执行进度和相关数据

其他建议

数据模型版本管理

  • 对Mapping进行版本管理
    • 包含在代码或者以专门文件进行管理, 添加好注释, 并加入GIt等版本管理仓库中, 方便回顾
    • 为每一个增加metadata字段, 在其维护的一些文档相关的元数据, 方便对数据进行管理

2020-04-16_013038

防止字段过多

  • 字段过多有如下坏处:
    • 难以维护, 当字段成百上千时, 基本很难有人能明确知道每个字段的含义
    • mapping的信息存储在cluster state里面, 过多的字段会导致mapping过大, 最终导致更新变慢
  • 通过index.mapping.total_field.limit可以限定索引中最大字段数, 默认是1000
  • 可以通过key/value的方式解决字段过多的问题, 但并不完美

key/value详解

2020-04-16_013426 2020-04-16_013504 2020-04-16_013534
  • 虽然可以通过这种方式极大地减少field数目, 但也有一些明显的坏处
    • query语句复杂度飙升, 且有一些可能无法实现, 比如聚合分析相关的
    • 不利于在kibana中做可视化分析
  • 一般字段过多的原因是由于没有高质量的数据建模导致的, 比如dynamic设置为了true
  • 考虑拆分多个索引来解决问题

   转载规则


《Elasticsearch篇之数据建模》 Jiavg 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录